﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.NRefactory.Editor;
using Peter.Common.Icons;
using Peter.Common.MainMenu;
using Peter.Logic;
using Peter.Logic.AvalonEdit;

namespace Peter.Controls
{
   /// <summary>
   /// Represents a code editor control in Peter.
   /// </summary>
   public class CodeEditor : TextEditor
   {
      private readonly HighlightCurrentLineBackgroundRenderer m_LineHighlighter;
      private readonly HighlightBracesBackgroundRenderer m_MatchRenderer;
      private CompletionWindow m_CompletionWindow;
      private OverloadInsightWindow m_InsightWindow;
      private bool m_IsCustomHighlight;
      private int m_OpenTries;

      /// <summary>
      /// Initializes a new code editor.
      /// </summary>
      public CodeEditor ()
      {
         this.CreateContextMenu ();
         this.CommandBindings.Add (new CommandBinding (PeterInternalCommands.ShowCodeCompletion, this.OnShowCodeCompletion));
         this.m_LineHighlighter = new HighlightCurrentLineBackgroundRenderer (this);
         this.m_MatchRenderer = new HighlightBracesBackgroundRenderer (this);
         this.TextArea.TextEntering += this.OnTextEntering;
         this.TextArea.TextEntered += this.OnTextEntered;
      }

      /// <summary>
      /// Gets the code completion.
      /// </summary>
      public BaseCodeCompletion CodeCompletion { get; private set; }

      /// <summary>
      /// Opens the given file.
      /// </summary>
      /// <param name="file">File to open.</param>
      public void OpenFile (FileInfo file)
      {
         try
         {
            if (this.m_OpenTries == 5) return; // TODO: Show error message...
            this.Load (file.FullName);
            this.Document.FileName = file.FullName;
            this.SetHighlighting (file);
            this.m_OpenTries = 0;
         }
         catch (Exception)
         {
            this.m_OpenTries++;
            Thread.Sleep (500);
            this.OpenFile(file);
         }
      }

      /// <summary>
      /// Saves the given file.
      /// </summary>
      /// <param name="fileInfo">File to save to.</param>
      public void SaveFile (FileInfo fileInfo)
      {
         this.Save (fileInfo.FullName);
         fileInfo.Refresh ();
         this.SetHighlighting (fileInfo);
      }

      /// <summary>
      /// Sets the highlighting based on the given file.
      /// </summary>
      /// <param name="file"></param>
      private void SetHighlighting (FileInfo file)
      {
         if (!this.m_IsCustomHighlight)
         {
            this.SyntaxHighlighting = file.Name.ToLower () == "gemfile"
               ? HighlightingManager.Instance.GetDefinition ("Ruby")
               : HighlightingManager.Instance.GetDefinitionByExtension (file.Extension);
         }

         if (this.CodeCompletion == null)
         {
            if (file.Extension == ".cs")
               this.CodeCompletion = new CSharpCodeCompletion ();
         }
      }

      /// <summary>
      /// Sets the highlighting of this editor.
      /// </summary>
      /// <param name="name">Name of highlighting to set.</param>
      public void SetHighlighting (string name)
      {
         this.m_IsCustomHighlight = true;
         this.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition (name);
      }

      /// <summary>
      /// Occurs when text is being entered in the editor.
      /// </summary>
      /// <param name="sender">this</param>
      /// <param name="e">TextCompositionEventArgs</param>
      private void OnTextEntering (object sender, TextCompositionEventArgs e)
      {
         if (e.Text.Length > 0 && this.m_CompletionWindow != null)
         {
            if (!char.IsLetterOrDigit (e.Text [0]))
            {
               this.m_CompletionWindow.CompletionList.RequestInsertion (e);
            }
         }
      }

      /// <summary>
      /// Occurs when text is has been entered in the editor.
      /// </summary>
      /// <param name="sender">this</param>
      /// <param name="e">TextCompositionEventArgs</param>
      private async void OnTextEntered (object sender, TextCompositionEventArgs e)
      {
         await this.ShowCompletion (e.Text);
      }

      /// <summary>
      /// Occurs when the control + space command has been executed.
      /// </summary>
      /// <param name="sender">Issuer of command.</param>
      /// <param name="e">ExecutedRoutedEventArgs</param>
      private async void OnShowCodeCompletion (object sender, ExecutedRoutedEventArgs e)
      {
         await this.ShowCompletion (null);
      }

      /// <summary>
      /// Attempts to show the code completion window.
      /// </summary>
      /// <param name="enteredText">The text that was entered.</param>
      private async Task ShowCompletion (string enteredText)
      {
         if (this.CodeCompletion == null) return;
         if (this.m_CompletionWindow == null)
         {
            int offset;
            var doc = GetCompletionDocument (out offset);
            var results = await this.CodeCompletion.GetCompletions (doc, offset, enteredText == null);
            if (results == null)
               return;
            if (this.m_InsightWindow == null && results.OverloadProvider != null)
            {
               this.m_InsightWindow = new OverloadInsightWindow (this.TextArea) { Provider = results.OverloadProvider };
               this.m_InsightWindow.Show ();
               this.m_InsightWindow.Closed += (sender, args) => this.m_InsightWindow = null;
            }

            if (results.CompletionData.Any ())
            {
               this.m_CompletionWindow = new CompletionWindow (this.TextArea) { CloseWhenCaretAtBeginning = enteredText == null };
               this.m_CompletionWindow.StartOffset -= results.TriggerWordLength;

               var data = this.m_CompletionWindow.CompletionList.CompletionData;
               foreach (var completion in results.CompletionData.OrderBy (item => item.Text))
                  data.Add (completion);
               if (results.TriggerWordLength > 0)
                  this.m_CompletionWindow.CompletionList.SelectItem (results.TriggerWord);
               this.m_CompletionWindow.Show ();
               this.m_CompletionWindow.Closed += (sender, args) => this.m_CompletionWindow = null;
            }
         }

         if (!string.IsNullOrEmpty (enteredText) && this.m_InsightWindow != null)
         {
            var provider = this.m_InsightWindow.Provider as CSharpOverloadProvider;
            if (provider != null)
            {
               //since the text has not been added yet we need to treat it as if the char has already been inserted
               int offset;
               var doc = GetCompletionDocument (out offset);
               provider.Update (doc, offset);
               //if the windows is requested to be closed we do it here
               if (provider.RequestClose)
               {
                  this.m_InsightWindow.Close ();
                  this.m_InsightWindow = null;
               }
            }
         }
      }

      /// <summary>
      /// Gets the document used for code completion, can be overridden to provide a custom document
      /// </summary>
      /// <param name="offset"></param>
      /// <returns>The document of this text editor.</returns>
      protected virtual IDocument GetCompletionDocument (out int offset)
      {
         offset = CaretOffset;
         return Document;
      }

      /// <summary>
      /// Closes this code editor.
      /// </summary>
      public void Close ()
      {
         this.TextArea.TextEntering -= this.OnTextEntering;
         this.TextArea.TextEntered -= this.OnTextEntered;
         this.m_MatchRenderer.Remove ();
         this.m_LineHighlighter.Remove ();
      }

      /// <summary>
      /// Creates the context menu for the code editor.
      /// </summary>
      private void CreateContextMenu ()
      {
         this.ContextMenu = new ContextMenu
         {
            Items =
            {
               new MenuItem
               {
                  Header = "Edit",
                  Icon = new IconDisplay { Icon = MainMenuIcon.Editalt, Margin = new Thickness (2) },
                  Items =
                  {
                     new MenuItem
                     {
                        Header = "To Uppercase",
                        Command = AvalonEditCommands.ConvertToUppercase,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "To Lowercase",
                        Command = AvalonEditCommands.ConvertToLowercase,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "To Title Case",
                        Command = AvalonEditCommands.ConvertToTitleCase,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "Invert Case",
                        Command = AvalonEditCommands.InvertCase,
                        CommandTarget = this.TextArea
                     },
                     new Separator (),
                     new MenuItem
                     {
                        Header = "Convert Leading Spaces To Tabs",
                        Command = AvalonEditCommands.ConvertLeadingSpacesToTabs,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "Convert Spaces To Tabs",
                        Command = AvalonEditCommands.ConvertSpacesToTabs,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "Convert Leading Tabs To Spaces",
                        Command = AvalonEditCommands.ConvertLeadingTabsToSpaces,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "Convert Tabs To Spaces",
                        Command = AvalonEditCommands.ConvertTabsToSpaces,
                        CommandTarget = this.TextArea
                     },
                     new Separator (),
                     new MenuItem
                     {
                        Header = "Remove Leading Whitespace",
                        Command = AvalonEditCommands.RemoveLeadingWhitespace,
                        CommandTarget = this.TextArea
                     },
                     new MenuItem
                     {
                        Header = "Remove Trailing Whitespace",
                        Command = AvalonEditCommands.RemoveTrailingWhitespace,
                        CommandTarget = this.TextArea
                     }
                  }
               },
               new Separator (),
               new MenuItem
               {
                  Header = "Undo",
                  Command = ApplicationCommands.Undo,
                  Icon = new IconDisplay { Icon = MainMenuIcon.Undo, Margin = new Thickness (2) }
               },
               new MenuItem
               {
                  Header = "Redo",
                  Command = ApplicationCommands.Redo,
                  Icon = new IconDisplay { Icon = MainMenuIcon.Repeat, Margin = new Thickness (2) }
               },
               new Separator (),
               new MenuItem
               {
                  Header = "Cut",
                  Command = ApplicationCommands.Cut,
                  Icon = new IconDisplay { Icon = MainMenuIcon.CutScissors, Margin = new Thickness (2) }
               },
               new MenuItem
               {
                  Header = "Copy",
                  Command = ApplicationCommands.Copy,
                  Icon = new IconDisplay { Icon = MainMenuIcon.Copy, Margin = new Thickness (2) }
               },
               new MenuItem
               {
                  Header = "Paste",
                  Command = ApplicationCommands.Paste,
                  Icon = new IconDisplay { Icon = MainMenuIcon.ClipboardPaste, Margin = new Thickness (2) }
               },
               new MenuItem
               {
                  Header = "Delete Line",
                  Command = AvalonEditCommands.DeleteLine,
                  CommandTarget = this.TextArea,
                  Icon = new IconDisplay { Icon = MainMenuIcon.Remove, Margin = new Thickness (2) }
               },
               new MenuItem
               {
                  Header = "Select All",
                  Command = ApplicationCommands.SelectAll,
                  Icon = new IconDisplay { Icon = MainMenuIcon.SelectionRectangleselection, Margin = new Thickness (2) }
               }
            }
         };
      }

      /// <summary>
      /// Sets the highlighting of the line.
      /// </summary>
      /// <param name="highlight">True to add highlighting, false to remove.</param>
      /// <param name="lineColor">Line highlight background color.</param>
      /// <param name="borderColor">Line highlight border color.</param>
      /// <param name="borderThickness">Line highlight border thickness.</param>
      public void SetLineHighlight (bool highlight, Color lineColor, Color borderColor, double borderThickness)
      {
         if (highlight)
         {
            this.m_LineHighlighter.HighlightBrush = new SolidColorBrush (lineColor);
            this.m_LineHighlighter.BorderPen = new Pen (new SolidColorBrush (borderColor), borderThickness);
            this.m_LineHighlighter.Add ();
         }
         else
            this.m_LineHighlighter.Remove ();
      }
   }
}
